Error Handling

undefined, null, void

undefined
  • var foo: u8 = undefined .

  • Should not be thought of as no value, but as a way of telling the compiler that you are not assigning a value yet .

  • Any type may be set to undefined, but attempting to read or use that value is always  considered a mistake.

null
  • var foo: ?u8 = null; .

  • The "null" primitive value is  a value that means "no value".

  • This is typically used with optional types as in the example above.

  • When foo  equals null , that's not a value of type u8 . It means there is no value  of type u8  in foo  at all.

void
  • var foo: void = {}; .

  • "void" is a type , not a value.

  • It is the most common of the Zero Bit Types (those types which take up absolutely no space and have only a semantic value).

  • When compiled to executable code, zero bit types generate no code at all.

  • The above example shows a variable foo  of type void  which is assigned the value of an empty expression.

  • It's much more common to see void  as the return type of a function that returns nothing.

Error Sets, Try, Catch

Error Sets
  • An error set is like an enum, where each error in the set is a value.

  • There are no exceptions in Zig; errors are values.

const FileOpenError = error{
    AccessDenied,
    OutOfMemory,
    FileNotFound,
};
Union: Error
  • An error set type and another type can be combined with the !  operator to form an error union type.

  • Values of these types may be an error value  or a value of the other type .

  • If you call a function that returns an error union ( !T ) without using try  or catch , the Zig compiler will emit a compile error, since there is no defined way to handle the possible error.

  • anyerror

    • Is the global error set, which due to being the superset of all error sets, can have an error from any set coerced to it. Its usage should generally be avoided.

  • In variables :

    const maybe_error: AllocationError!u16 = 10;
        // `maybe_error` can be a `u16` or an error of type `AllocationError`.
        // `AllocationError!u16` means the type can be `AllocationError` or `u16`.
    const no_error = maybe_error catch 0;
        // If `maybe_error` contained an error, `no_error` would receive `0`.
        // Since `maybe_error` does not contain an error, `no_error` equals 10.
    
  • In functions :

    • With AnyError :

      fn mightFail(x: bool) !i32 { 
          // `!i32` means the type can be **any error** or `i32`.
          if (x) {
              return error.SomeError; 
          } 
          return 42;
      }
      
    • With ErrorSet :

      const MyErrors = error{ OutOfMemory, InvalidInput };
      
      fn example(x: bool) MyErrors!i32 {
          if (x) {
              return error.OutOfMemory;      // valid.
              //return error.InvalidInput;   // valid.
              //return error.SomeOtherError; // invalid.
          }
          return 42;
      }
      
      
  • Merge :

    const A = error{ NotDir, PathNotFound };
    const B = error{ OutOfMemory, PathNotFound };
    const C = A || B;
    
Try
  • Used to propagate errors automatically .

    • If the operation results in an error, the error will be returned to the caller.

  • Note :

    • Zig's try  and catch  are unrelated to try-catch in other languages.

    • Zig does not let us ignore error unions via _ = x; . We must unwrap it with try , catch , or if  by some means.

      • _ = try x;  or _ = x catch {};  is possible.

  • Syntax sugar for |err| :

    • try x  is shorthand for x catch |err| return err .

Catch
  • catch  is used to handle errors directly , providing an alternative value or specific handling.

    • In other words, a 'fallback value'.

    • "Could instead be a noreturn  - the type of return , while (true)  and others."

  • Basic :

    const result = mightFail(false) catch -1; // If there is an error, result is -1.
    
  • With Payload Capturing :

    fn failingFunction() error{Oops}!void {
        return error.Oops;
    }
    
    fn main() !void {
        failingFunction() catch |err| {
            return;
        };
    }
    
  • With Payload Capturing and Blocks :

    • "If you want to provide a default value with catch  after performing some logic, you can combine catch  with named Blocks :"

    • Source .

    const a: ?std.json.Parsed(std.json.Value) = parseJson(allocator, "mapa/mapa.ldtkasd") catch |err| blk: {
    
        print("File not found {}\n", .{err});
    
        break :blk null;
    
    };
    
  • With Payload Capturing and Switch :

    fn mightFail(x: bool) !i32 {
        if (x) return 42;
        return error.SomeError;
    }
    
    pub fn main() void {
        const result = mightFail(false) catch |err| switch (err) {
            error.SomeError => -1, // Convert the error to -1
            else => -999, // Capture other errors
        };
    }
    
  • My examples :

    • Potentially crashing the program if an error occurs:

      const jsonParsed = try parseJson(allocator, mapa);
      defer jsonParsed.deinit();
      
    • Trying to continue program execution if an error occurs:

      const jsonParsed: ?std.json.Parsed(std.json.Value) = parseJson(allocator, mapa) catch |err| a: {
          print("\nERROR | LDtkParser: {}, {s}\n", .{err, mapa});
          break :a null;
      };
      if (jsonParsed) |*jsonParsed_| {
          defer jsonParsed_.*.deinit();
          //etc.
      }
      

Optionals

Union: Optionals ( ?T )
  • Used to store either null  or a value of type T .

  • Unwrapping :

    • .?  is a shorthand for orelse unreachable .

      • This is used when you know it is impossible for an optional value to be null; using this to unwrap a null  value is detectable illegal behaviour.

      const expect = @import("std").testing.expect;
      
      test "orelse unreachable" {
          const a: ?f32 = 5;
          const b = a orelse unreachable;
          const c = a.?;
          try expect(b == c);
          try expect(@TypeOf(c) == f32);
      }
      
    • orelse :

      • Acts when the optional is null . This unwraps the optional to its child type.

      const expect = @import("std").testing.expect;
      
      test "orelse" {
          const a: ?f32 = null;
          const fallback_value: f32 = 0;
          const b = a orelse fallback_value;
          try expect(b == 0);
          try expect(@TypeOf(b) == f32);
      }
      
    • Unwrapping in expressions and loops :

      • All uses of Payload Capturing .

      • If :

        const a: ?i32 = 5;
        // Method 1
        if (a != null) {
            const value = a.?;
            _ = value;
        }
        // Method 2
        var b: ?i32 = 5;
        if (b) |*value| {
            value.* += 1;
        }
        
      • While :

        var numbers_left: u32 = 4;
        fn eventuallyNullSequence() ?u32 {
            if (numbers_left == 0) return null;
            numbers_left -= 1;
            return numbers_left;
        }
        
        fn main() !void {
            var sum: u32 = 0;
            while (eventuallyNullSequence()) |value| {
                sum += value;
            }
        }
        
      • As in the union example, the captured value is immutable, but we can still use a pointer capture to modify the value stored in b .

  • Note :

    • Optional pointer and optional slice types do not take up any extra memory compared to non-optional ones.

      • This is because internally they use the 0 value of the pointer for null .

Runtime Safety, Unreachable

Detectable illegal behaviour
  • Illegal Behaviours .

  • Illegal behaviour will be caught (causing a panic) with safety on, but will result in undefined behaviour with safety off.

  • Users are strongly recommended to develop and test their software with safety on, despite its speed penalties.

  • Enabled:

    test "out of bounds" {
        const a = [3]u8{ 1, 2, 3 };
        var index: u8 = 5;
        const b = a[index];
    
        _ = b;
        index = index;
    }
    
  • Disabled:

    test "out of bounds, no safety" {
        @setRuntimeSafety(false);
        const a = [3]u8{ 1, 2, 3 };
        var index: u8 = 5;
        const b = a[index];
    
        _ = b;
        index = index;
    }
    
Unreachable
  • unreachable  is an assertion  to the compiler that this statement will not be reached.

  • It can tell the compiler that a branch is impossible, which the optimiser can then take advantage of.

  • Reaching an unreachable  is detectable illegal behaviour.

test "unreachable" {
    const x: i32 = 1;
    const y: u32 = if (x == 2) 5 else unreachable;  // crashes if `unreachable` is reached.
    _ = y;
}
const expect = @import("std").testing.expect;

fn asciiToUpper(x: u8) u8 {
    return switch (x) {
        'a'...'z' => x + 'A' - 'a',
        'A'...'Z' => x,
        else => unreachable,    // crashes if `unreachable` is reached.
    };
}

test "unreachable switch" {
    try expect(asciiToUpper('a') == 'A');
    try expect(asciiToUpper('A') == 'A');
}